package com.gitee.wsl.time

import com.gitee.wsl.time.T.HOURS_PER_DAY
import com.gitee.wsl.time.T.MINUTES_PER_HOUR
import com.gitee.wsl.time.T.NANOSECONDS_PER_HOUR
import com.gitee.wsl.time.T.NANOSECONDS_PER_MICROSECOND
import com.gitee.wsl.time.T.NANOSECONDS_PER_MILLISECOND
import com.gitee.wsl.time.T.NANOSECONDS_PER_MINUTE
import com.gitee.wsl.time.T.NANOSECONDS_PER_SECOND
import com.gitee.wsl.time.T.SECONDS_PER_HOUR
import com.gitee.wsl.time.T.SECONDS_PER_MINUTE
import com.gitee.wsl.unit.angle.AngleUnit
import com.gitee.wsl.unit.angle.toDegrees
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.format
import kotlinx.datetime.format.AmPmMarker
import kotlinx.datetime.format.char
import kotlin.jvm.JvmName
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.min
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime

@ExperimentalTime
val LocalTime.millisecond: Int
    get() = nanosecond / 1_000_000

fun LocalTime.Companion.of(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0) = LocalTime(hour, minute, second, nanosecond)


private val _max = LocalTime(23, 59, 59, 999_999_999)
private val _min = LocalTime(0, 0)
private val _noon = LocalTime(12, 0)

val LocalTime.Companion.NOON: LocalTime
    get() = _noon

val LocalTime.Companion.MAX: LocalTime
    get() = _max

fun LocalTime.Companion.time() = LocalTime.now(TimeZone.currentSystemDefault())
fun LocalTime.Companion.nextMinute() = time() + 1.minutes
fun LocalTime.Companion.nextHour() = time() + 1.hours
fun LocalTime.Companion.lastMinute() = time() - 1.minutes
fun LocalTime.Companion.lastHour() = time() - 1.hours


val LocalTime.Companion.MIDNIGHT: LocalTime
    get() = _min


val LocalTime.Companion.MIN: LocalTime
    get() = _min




/**
 * The second of the day.
 */
val LocalTime.secondOfDay: Int
    get() = hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second

/**
 * The number of seconds since the start of the day.
 */
val LocalTime.secondsSinceStartOfDay
    get() = secondOfDay.seconds

/**
 * The nanosecond of the day.
 */
val LocalTime.nanosecondOfDay: Long
    get() {
        return  hour.toLong() * NANOSECONDS_PER_HOUR +
                minute.toLong() * NANOSECONDS_PER_MINUTE +
                second.toLong() * NANOSECONDS_PER_SECOND +
                nanosecond
    }

/**
 * The number of nanoseconds since the start of the day.
 */
val LocalTime.nanosecondsSinceStartOfDay
    get() = nanosecondOfDay.nanoseconds

/**
 * Create a new [LocalDateTime] using given [date]
 */
 fun LocalTime.atDate(date: LocalDate): LocalDateTime = LocalDateTime(date, this)

/**
 * Returns an integer that represents this time, like a string, but without the ":"
 *
 * Truncates nanoseconds.
 *
 * Examples:
 * - 17:12:45 -> 171245
 * - 00:00:00 -> 0
 * - 05:00:00 -> 50000
 */
 fun LocalTime.toInt(): Int = hour * 10000 + minute * 100 + second

// ---- operations

/**
 * Calculate distance to the [other] between [LocalTime.MIN] and [LocalTime.MAX]
 */
 infix fun LocalTime.distanceTo(other: LocalTime): LocalTime = LocalTime.fromSecondOfDay(
    (other.toSecondOfDay() - toSecondOfDay()).absoluteValue
)


/**
 * Returns this object as [kotlin.time.Duration]
 */
 val LocalTime.asDuration: Duration get() = toNanosecondOfDay().nanoseconds

/**
 * @return the amount of seconds from this object's value [to]
 * The amount is always positive
 */
 infix fun LocalTime.distanceInSeconds(to: LocalTime): Int =
    abs(to.hour - hour) * 60 * 60 + abs(to.minute - minute) * 60 + abs(to.second - second)

/**
 * Whether the hours cross the PM threshold (hour >= 12)
 */
 val LocalTime.isPM: Boolean get() = hour >= 12

/** * Get the amount of minutes since midnight.
 */
 val LocalTime.totalMinutes: Double
    get() = hour * 60 + minute + second.toDouble() / 60

/**
 * Get the amount of seconds since midnight. Convenient for storing [LocalTime] as an [Int] value
 */
 val LocalTime.totalSeconds: Int
    get() = hour * 3600 + minute * 60 + second

/**
 * Get hour in am/pm format (just the number)
 */
 val LocalTime.hourAs12H: Int
    get() = if (hour % 12 == 0) hour else hour % 12

/**
 * Add specified number of [hours], [minutes], or [seconds] to this time.
 * If the argument is not specified, 0 will be added.
 * **If the resulting value is bigger than 23:59:59,
 * the value will be wrapped (e.g. to 00:00:00)**
 * @return A new resulting [LocalTime] object.
 */
 fun LocalTime.add(hours: Int = 0, minutes: Int = 0, seconds: Int = 0): LocalTime {
    val (normalizedHours, normalizedMinutes, normalizedSeconds) = LocalTime.normalize(hours, minutes, seconds)

    val hDelta =
        (normalizedHours + hour + (minute + normalizedMinutes) / 60 + (second + normalizedSeconds) / 3600) % 24
    val mDelta = (minute + normalizedMinutes + (second + normalizedSeconds) / 60) % 60
    val sDelta = (second + normalizedSeconds) % 60

    val h = (if (hDelta >= 0) hDelta else hDelta + 24) % 24
    val m = if (mDelta >= 0) mDelta else mDelta + 60
    val s = if (sDelta >= 0) sDelta else sDelta + 60
    return LocalTime(h, m, s, nanosecond)
}

/**
 * Represents this numeric value as if it is a number on the clock
 * Examples:
 * - 0 -> "00"
 * - 1 -> "01"
 * - 15 -> "15"
 */
 fun LocalTime.Companion.asTimeNumber(value: Int): String = if (value < 10) "0$value" else value.toString()

// ---- builders

/**
 * Create a new [LocalTime] from [duration].
 * If the [duration] is larger than 24 hours, it will wrap around
 */
 operator fun LocalTime.Companion.invoke(duration: Duration): LocalTime =
    fromNanosecondOfDay(duration.inWholeNanoseconds)

/**
 * Parse a new time object using the int representation of it.
 * @see LocalTime.toInt
 */
 fun LocalTime.Companion.ofInt(hms: Int): LocalTime = LocalTime(hms / 10000, hms / 100 % 100, hms % 100)

/**
 * Create a time from milliseconds since midnight.
 * **[millis] is NOT a timestamp**
 */
 fun LocalTime.Companion.ofMillis(millis: Int): LocalTime = fromMillisecondOfDay(millis)

/**
 * Get current time value using specified seconds **since midnight**
 * [seconds] is NOT a timestamp
 * @see totalSeconds
 */
 fun LocalTime.Companion.ofSeconds(seconds: Int): LocalTime = fromSecondOfDay(seconds)

/**
 * Create a new time using specified [hours], [minutes], or [seconds]
 * If you specify a value bigger than the initial, the remainder will spill into the next
 * value. If you specify a value bigger than [LocalTime.MAX], it will wrap around.
 * Examples:
 * - `LocalTime.with(seconds=70)` -> 00:01:10
 * - `LocalTime.with(hours=25, minutes=60)` -> 02:00:00
 */
 fun LocalTime.Companion.with(
    hours: Int = 0,
    minutes: Int = 0,
    seconds: Int = 0
): LocalTime = MIN.add(hours, minutes, seconds)

// ---- Parsing

/**
 * Parse [s] as time serialized using [LocalTime.toString] and return null if it fails.
 * @see [LocalTime.Companion.parse]
 */
 fun LocalTime.Companion.parseOrNull(s: String): LocalTime? = s.runCatching { parse(this) }.getOrNull()

/**
 * Returns true if [time] is a valid time string
 */
 fun LocalTime.Companion.isValid(time: String): Boolean = parseOrNull(time) != null

/**
 * Returns true if this is a valid time string
 */
 val String?.isValidLocalTime: Boolean get() = this?.let { LocalTime.isValid(it) } ?: false

/**
 * Try to parse [this] as [LocalTime] or return null if it fails
 */
 fun String?.toLocalTimeOrNull(): LocalTime? = this?.let { LocalTime.parseOrNull(this) }

// ---- collection

/**
 * Compute total asDuration of all items in the collection
 */
 fun Iterable<LocalTime?>.totalDuration(): Duration = sumOf { it?.toNanosecondOfDay() ?: 0 }.nanoseconds

/**
 * Compute total asDuration of all items in the sequence
 */
@JvmName("totalDurationSequence")
 fun Sequence<LocalTime?>.totalDuration(): LocalTime = LocalTime.ofSeconds(sumOf { it?.totalSeconds ?: 0 })


/**
 * If [this] is [LocalTime.MIN] return null, [this] otherwise
 */
 fun LocalTime?.takeIfNotZero(): LocalTime? = takeIf { it != LocalTime.MIN }

/** Same as toString but gives you a choice on whether to use 12H scheme.
 * [toString] uses asString(false).
 *
 * @param addSecondsIfZero If the seconds value is equal to 0, should include them in the representation? e.g.:
 * @param use12h whether to represent time in 12-hour format (AM/PM letters not added). Mind that deserializing
 * the resulting string back to [LocalTime] won't result in a valid value, if you have this parameter true
 * true => "17:00:00", false => "17:00".
 *
 * @return a string representation of this time
 * **/
 fun LocalTime.asString(use12h: Boolean = false, addSecondsIfZero: Boolean = false): String = buildString {
    append("${LocalTime.asTimeNumber(if (use12h) hourAs12H else hour)}:${LocalTime.asTimeNumber(minute)}")
    if (addSecondsIfZero || second != 0) append(":${LocalTime.asTimeNumber(second)}")
    if (use12h) append(" ${if (isPM) "PM" else "AM"}")
}

/**
 * Create a new time with the result of adding [LocalTime] to [other]
 */
operator fun LocalTime.plus(other: LocalTime): LocalTime = add(other.hour, other.minute, other.second)

operator fun LocalTime.plus(duration: Duration): LocalTime {
    val rollOver = this.minute + (duration.inWholeMinutes % 60) > 60
    return LocalTime(
        hour = min(23, this.hour + duration.inWholeHours.toInt() + if (rollOver) 1 else 0),
        minute = this.minute + (duration.inWholeMinutes.toInt() % 60) % 60
    )
}

/**
 * Subtract [other] from [LocalTime]
 */
operator fun LocalTime.minus(other: LocalTime): LocalTime = add(-other.hour, -other.minute, -other.second)

operator fun LocalTime.minus(other: Duration): LocalTime = minus(other.toLocalTime())

fun LocalTime.next() = this + 1.milliseconds

fun LocalTime.previous() = this - 1.milliseconds


/**
 * Get either the hour or minute or second value of this time.
 * Values more than 2 will throw an [IndexOutOfBoundsException]
 */
 operator fun LocalTime.get(index: Int): Int = when (index) {
    0 -> hour
    1 -> minute
    2 -> second
    else -> throw IndexOutOfBoundsException("Only 0, 1 and 2 are valid values")
}


/** example: 12:45:00, 4:30, 7:00 AM, 24 or 12h format, word separator is " ".
 * On a value that is not a valid time, will throw.
 * **/
@Throws(IllegalArgumentException::class)
 fun LocalTime.Companion.parseAs12H(s: String): LocalTime {
    try {
        require(s.isNotBlank()) { "Empty time string" }

        val words = s.split(" ")
        require(words.size in 1..2) { "Not a time" }

        val parts = words.first().split(':', '.', '-', ' ', ',', '_', ignoreCase = true)
        require(parts.size in 2..3) { "Invalid delimiter count" }

        val hours = parts[0].toInt() + if (words.size == 2 && words[1] == "PM") 12 else 0

        val minutes = parts[1].toInt()

        val seconds = if (parts.size == 3) parts[2].toInt() else 0

        return LocalTime(hours, minutes, seconds)
    } catch (expected: Exception) {
        throw IllegalArgumentException("Couldn't parse time", expected)
    }
}

fun LocalTime.convertTo12HourFormat(): Pair<Int, MeridiemPeriod> {
    val hour = this.hour
    val period = if (hour < 12) MeridiemPeriod.AM else MeridiemPeriod.PM
    val hour12 = (hour % 12).let { if (it == 0) 12 else it }
    return Pair(hour12, period)
}

/** Normalize a set of values for hours, minutes, or seconds, to a valid time value.
 *
 * @returns values of hours, minutes and seconds adjusted if necessary to fit into their respective
 * ranges. Any excess is added to the value of the next order. The values can also wrap
 * around if the resulting time is bigger than [LocalTime.MAX]
 * Example: normalize(25,70,100) -> (2,11,40)
 */
internal fun LocalTime.Companion.normalize(hours: Int = 0, minutes: Int = 0, seconds: Int = 0): Triple<Int, Int, Int> {
    val normalizedHours = hours + (minutes + seconds / 60) / 60
    val normalizedMinutes = (minutes + seconds / 60) % 60
    val normalizedSeconds = seconds % 60
    return Triple(normalizedHours % 24, normalizedMinutes, normalizedSeconds)
}

/**
 * Get the current time using provided [zone].
 */
 fun LocalTime.Companion.now(zone: TimeZone): LocalTime = LocalDateTime.now(zone).time

/**
 * Returns the same time with [LocalTime.nanosecond] set to 0.
 */
 fun LocalTime.truncateNanos(): LocalTime = LocalTime(hour, minute, second, 0)

/**
 * Returns the same time with both nanos and seconds set to 0.
 */
 fun LocalTime.truncateSeconds(): LocalTime = LocalTime(hour, minute, 0, 0)

/**
 * Returns the same time with all of nanos, seconds and minutes set to 0
 */
fun LocalTime.truncateMinutes(): LocalTime = LocalTime(hour, 0, 0, 0)


fun LocalTime.roundedToNearestUnchecked(increment: Duration): LocalTime {
    val remainder = nanosecondsSinceStartOfDay % increment

    return if (remainder > Duration.ZERO) {
        if (remainder < increment / 2) {
            this - remainder.toLocalTime()
        } else {
            this + (increment - remainder).toLocalTime()
        }
    } else {
        this
    }
}


/**
 * Returns this time, rounded to match the precision of a given [unit]. If the time is halfway between whole values of
 * the unit, it will be rounded up.
 */
fun LocalTime.roundedTo(unit: DurationUnit): LocalTime {
    return when (unit) {
        DurationUnit.DAYS -> LocalTime.MIDNIGHT
        DurationUnit.HOURS -> if (minute >= 30) nextWholeHour() else previousWholeHour()
        DurationUnit.MINUTES -> if (second >= 30) nextWholeMinute() else previousWholeMinute()
        DurationUnit.SECONDS -> if (nanosecond >= 500_000_000) nextWholeSecond() else previousWholeSecond()
        DurationUnit.MILLISECONDS -> roundedToNearestUnchecked(1.milliseconds)
        DurationUnit.MICROSECONDS -> roundedToNearestUnchecked(1.microseconds)
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}

internal fun LocalTime.roundedUpToNearestUnchecked(increment: Duration): LocalTime {
    val remainder = nanosecondsSinceStartOfDay % increment
    return if (remainder > Duration.ZERO) this + (increment - remainder).toLocalTime() else this
}

/**
 * Returns this time, rounded up to match the precision of a given [unit].
 */
fun LocalTime.roundedUpTo(unit: DurationUnit): LocalTime {
    return when (unit) {
        DurationUnit.DAYS -> LocalTime.MIDNIGHT
        DurationUnit.HOURS -> if (minute > 0 || second > 0 || nanosecond > 0) nextWholeHour() else this
        DurationUnit.MINUTES -> if (second > 0 || nanosecond > 0) nextWholeMinute() else this
        DurationUnit.SECONDS -> if (nanosecond > 0) nextWholeSecond() else this
        DurationUnit.MILLISECONDS -> roundedUpToNearestUnchecked(1.milliseconds)
        DurationUnit.MICROSECONDS -> roundedUpToNearestUnchecked(1.microseconds)
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}


/**
 * Returns this time, rounded down to match the precision of a given [unit].
 *
 * This is equivalent to [truncatedTo].
 */
fun LocalTime.roundedDownTo(unit: DurationUnit): LocalTime {
    return when (unit) {
        DurationUnit.DAYS -> LocalTime.MIDNIGHT
        DurationUnit.HOURS -> previousWholeHour()
        DurationUnit.MINUTES -> previousWholeMinute()
        DurationUnit.SECONDS -> previousWholeSecond()
        DurationUnit.MILLISECONDS -> previousWholeMillisecond()
        DurationUnit.MICROSECONDS -> previousWholeMicrosecond()
        DurationUnit.NANOSECONDS -> this
        else -> this
    }
}


internal fun LocalTime.roundedDownToNearest(increment: Duration): LocalTime {
    val remainder = nanosecondsSinceStartOfDay % increment
    return if (remainder > Duration.ZERO) this - remainder.toLocalTime() else this
}


internal fun LocalTime.copyIfChanged(nanosecond: Int): LocalTime {
    return if (nanosecond == this.nanosecond) this else copy(nanosecond = nanosecond)
}

/**
 * Returns a copy of this time with the values of any individual components replaced by the new values
 * specified.
 * @throws DateTimeException if the resulting time is invalid
 */
fun LocalTime.copy(
    hour: Int = this.hour,
    minute: Int = this.minute,
    second: Int = this.second,
    nanosecond: Int = this.nanosecond
) = LocalTime(hour, minute, second, nanosecond)


fun LocalTime.nextWholeHour() = nextWholeHourOrNull() ?: LocalTime.MIDNIGHT
fun LocalTime.nextWholeMinute() = nextWholeMinuteOrNull() ?: LocalTime.MIDNIGHT
fun LocalTime.nextWholeSecond() = nextWholeSecondOrNull() ?: LocalTime.MIDNIGHT

fun LocalTime.previousWholeHour(): LocalTime {
    return if (minute > 0 || second > 0 || nanosecond > 0) {
        copy(minute = 0, second = 0, nanosecond = 0)
    } else {
        this
    }
}

fun LocalTime.previousWholeMinute(): LocalTime {
    return if (second > 0 || nanosecond > 0) {
        copy(second = 0, nanosecond = 0)
    } else {
        this
    }
}

fun LocalTime.previousWholeSecond(): LocalTime {
    return if (nanosecond > 0) {
        copy(nanosecond = 0)
    } else {
        this
    }
}

fun LocalTime.previousWholeMillisecond(): LocalTime {
    return copyIfChanged(nanosecond = nanosecond / NANOSECONDS_PER_MILLISECOND * NANOSECONDS_PER_MILLISECOND)
}

fun LocalTime.previousWholeMicrosecond(): LocalTime {
    return copyIfChanged(nanosecond = nanosecond / NANOSECONDS_PER_MICROSECOND * NANOSECONDS_PER_MICROSECOND)
}

fun LocalTime.nextWholeHourOrNull(): LocalTime? {
    val newHour = hour + 1

    return if (newHour == HOURS_PER_DAY) {
        null
    } else {
        LocalTime(hour = newHour, minute = 0)
    }
}

fun LocalTime.nextWholeMinuteOrNull(): LocalTime? {
    var newMinute = minute + 1
    var newHour = hour

    if (newMinute == MINUTES_PER_HOUR) {
        newMinute = 0
        newHour = hour + 1

        if (newHour == HOURS_PER_DAY) {
            return null
        }
    }

    return LocalTime(newHour, newMinute)
}

fun LocalTime.nextWholeSecondOrNull(): LocalTime? {
    var newSecond = second + 1
    var newMinute = minute
    var newHour = hour

    if (newSecond == SECONDS_PER_MINUTE) {
        newSecond = 0
        newMinute = minute + 1

        if (newMinute == MINUTES_PER_HOUR) {
            newMinute = 0
            newHour = hour + 1

            if (newHour == HOURS_PER_DAY) {
                return null
            }
        }
    }

    return LocalTime(newHour, newMinute, newSecond)
}

fun LocalTime.getSecondsInDegrees(floatPrecision: Boolean):AngleUnit {
    return if (floatPrecision) {
        ((second + nanosecond / 1000000000f) * 6f).toDegrees()
    } else {
        (second * 6f).toDegrees()
    }
}

fun LocalTime.getMinutesInDegrees():AngleUnit {
    return ((minute + second / 60f) * 6f).toDegrees()
}

fun LocalTime.getHoursInDegrees():AngleUnit {
    return (hour * (360.0F / 12F) + minute * (360.0F / 12F / 60F)).toDegrees()
}

fun LocalTime.getHoursInDegrees24():AngleUnit {
    return (hour * (360.0F / 24F) + minute * (360.0F / 24F / 60F)).toDegrees()
}

fun LocalTime.toString(
    is24HourFormat: Boolean = true,
): String {
    return format(
        if (is24HourFormat) {
            LocalTime.Format {
                hour()
                char(':')
                minute()
            }
        } else {
            LocalTime.Format {
                amPmHour()
                char(':')
                minute()
                char(' ')
                amPmMarker(
                    AmPmMarker.AM.toString(),
                    AmPmMarker.PM.toString(),
                )
            }
        },
    )
}
